
package Gui;

import static controller.Globals.TRIANGLE_WINDOW_HEIGHT;
import static controller.Globals.TRIANGLE_WINDOW_WIDTH;
import static controller.Globals.VERTEX_SIZE_HIGHLIGHTED;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Callable;

import javax.swing.JPanel;
import javax.swing.Scrollable;
import Triangulation.EdgeGenerator;
import Triangulation.FastLMTTriangulator;
import Triangulation.LMTTriangulator;

import controller.Globals;
import static controller.Globals.*;
import controller.Notification;
import controller.NotificationCenter;
import controller.NotificationObserver;

import datatypes.Circle;
import datatypes.Edge;
import datatypes.Vertex;

public class TriangleDisplay extends JPanel implements MouseListener, MouseMotionListener, KeyListener, Scrollable, NotificationObserver
{
	private static final long serialVersionUID = -8006175897441108518L;
	
	
	MainWindow mainWindow;
	public List<Circle> circles = new ArrayList<Circle>();
	private boolean showCircles = false;
	private List<Edge> edges = null;
	private Vector<Vertex> vertices = new Vector<Vertex>();
	private Set<Vertex> selected = new HashSet<Vertex>();
	private Set<Vertex> pasteBuffer = new HashSet<Vertex>();
	private Vertex vertexDisplayingCoordinates = null;
	private double zoom = 1.0;
	private Boolean computing = false;
	private Boolean needsComputing = true;
	private boolean wantsPolygonTriangulation = true;
	private EdgeGenerator triangulator;
	private boolean selectionDrag = false;
	
	
	private Vertex clickPoint;
	private Vertex dragPoint;
	
	private AffineTransform transform = new AffineTransform();

	public TriangleDisplay(MainWindow main)
	{
		super();
		addMouseListener(this); 
        addMouseMotionListener(this); 
        addKeyListener(this);
        NotificationCenter nc = NotificationCenter.defaultNotificationCenter();
        nc.addObserver(Notification.didFindEdgeIn, this);
        nc.addObserver(Notification.didFinishMWT, this);
        nc.addObserver(Notification.didTriangulatePolygon, this);
        nc.addObserver(Notification.didTerminateTriangulator, this);
        nc.addObserver(Notification.didFindCircles, this);
        mainWindow = main;
        this.setPreferredSize(new Dimension(TRIANGLE_WINDOW_WIDTH,TRIANGLE_WINDOW_HEIGHT));
        this.setBackground(Color.white);
	}
	
	public void setZoom(double z) {
		zoom = z;
		if (z > 1.0)
			setPreferredSize( new Dimension( (int)(z * TRIANGLE_WINDOW_WIDTH), (int)( z * TRIANGLE_WINDOW_HEIGHT ) ));
		repaint();
		doLayout();
		mainWindow.doLayout();
		revalidate();
	}
	
	public void spawnRandomPoints(int numPoints) 
    { 
        synchronized(vertices) 
        { 
            vertices.clear();
            if (edges != null) {
            	edges.clear();
			}
            selected.clear();
            vertices.addAll(tools.RandomVerticesGenerator.randomVertices(getViewportWidth(), getViewportHeight(), 5.0, numPoints)); 
            System.out.println(vertices.size());
            triangulate(true);
        } 
    } 
     
    public void spawnLoadPoints(String state) 
    { 
        synchronized(vertices) 
        { 
            vertices.clear();
            selected.clear();
            if (edges != null) {
            	edges.clear();
			}
            try { 
                vertices.addAll(tools.OpenSaveVertices.loadVertices(state)); 
            } catch (IOException e) { 
                // TODO Auto-generated catch block 
                e.printStackTrace(); 
            } catch (ClassNotFoundException e) { 
                // TODO Auto-generated catch block 
                e.printStackTrace(); 
            } 
            System.out.println(vertices.size());
            triangulate(true); 
        } 
    } 
     
    public void spawnSavePoints(String state) 
    { 
        synchronized(vertices) 
        { 
            Vector<Vertex> vertsToSave = new Vector<Vertex>(); 
            for (int i = 0; i < vertices.size(); i++) vertsToSave.addElement(vertices.get(i)); 
            try { 
                tools.OpenSaveVertices.saveVertices(vertsToSave, state); 
            } catch (IOException e) { 
                // TODO Auto-generated catch block 
                e.printStackTrace(); 
            } 
        } 
    } 
     
    
    public void redraw(){
    	this.repaint();
    }
    
    Vector<Vertex> getVertices() 
    { 
        return vertices; 
    }
    
//    public void setVertices(Vector<Vertex> vertices) {
//		this.vertices = vertices;
//	}
     
    public int getViewportWidth() 
    {
    	return this.getWidth(); 
    } 
    public int getViewportHeight() 
    {
    	return this.getHeight(); 
    }
	
	
	
	public void showMesh(List<Edge> edges)
	{
		this.edges = edges;
		updateUI();
	}	
	
	public void reset()
	{
		this.edges = null;
		this.vertices.clear();
		this.selected.clear();
		this.circles.clear();
		updateUI();		
	}
	
	public void triangulate(boolean triangulatePolygons) {
		synchronized (computing) {
			needsComputing = true;
			wantsPolygonTriangulation = triangulatePolygons;
		}
		processChanges();
	}
	
	public void processChanges() 
    {
        updateUI();
        
        if (vertices.size() < 4) {
			return;
		}
        
        synchronized (computing) {
        	if (!needsComputing) {
				return;
			}
	        if (computing) {
	        	needsComputing = true;
				return;
			}
	        this.circles.clear();
			computing = true;
			needsComputing = false;
		}
        
//        mainWindow.getBottomPanel().updateProgress("Processing...", 1);
        NotificationCenter nc = NotificationCenter.defaultNotificationCenter();
        nc.postNotification(Notification.willStartProcessing, this);
        
        ExecutorService executor = Globals.mainExecutor;
        executor.submit(new Callable<List<Edge>>() {
        	public List<Edge> call() throws Exception {
        		Vertex[] aVertices = new Vertex[vertices.size()]; 
                vertices.toArray(aVertices);
        		triangulator = mainWindow.getActiveTriangulator();
        		List<Edge> mwt;
        		if (triangulator instanceof LMTTriangulator) {
        			LMTTriangulator t = (LMTTriangulator) triangulator;
					mwt = t.processVertices(aVertices, mainWindow.getActiveMetric(), wantsPolygonTriangulation);
				} else {
					mwt = triangulator.processVertices(aVertices, mainWindow.getActiveMetric());
				}
        		synchronized (computing) {
					computing = false;
				}
        		return mwt;
        	}
        });
    } 

	public void paintComponent(Graphics graphics)
	{
		super.paintComponent(graphics);
		
		Graphics2D g = (Graphics2D) graphics.create();
		AffineTransform savedTransform = g.getTransform();
		AffineTransform toCenterTransform = new AffineTransform();
		transform.setToScale(zoom, zoom);
		toCenterTransform.concatenate(transform);
		g.transform(toCenterTransform);
		int width = getWidth();
		int height = getHeight();
		g.setColor(Color.white);
		g.fillRect(0, 0, width, height);

		if (showCircles && circles != null) {
			for (int i = 0; i < circles.size(); i++) {
				Circle circle = circles.get(i);
				circle.draw(g, this);
			}
		}

		if (edges != null) {
			for (int i = 0; i < edges.size(); i++)
			{
				Edge edge = edges.get(i);
				if (edge != null) {
					edge.draw(g, this);
				}
			}
		}
		
		if (vertices != null) {
			g.setColor(Color.black);
			
			for (Vertex v : vertices) {
				v.draw(g, this);
			}
		}
		
		if (selectionDrag) {
			
			g.setColor(Color.green.darker());
			float dash[] = { 5.0f };
			g.setStroke(new BasicStroke(0.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f));
			int w = dragPoint.getX().intValue() - clickPoint.getX().intValue();
			int h = dragPoint.getY().intValue() - clickPoint.getY().intValue();
			int x, y;
			if (w < 0) {
				w = -w;
				x = dragPoint.getX().intValue();
			} else {
				x = clickPoint.getX().intValue();
			}
			if (h < 0) {
				h = -h;
				y = dragPoint.getY().intValue();
			} else {
				y = clickPoint.getY().intValue();
			}
			g.drawRect(x, y, w, h);
		}
		
		g.setTransform(savedTransform);
	}
	
	
    @Override 
    public void mouseClicked(MouseEvent arg0) 
    {
    	
    } 
 
    @Override 
    public void mouseEntered(MouseEvent arg0) 
    { 
        requestFocusInWindow(); 
    } 
 
    @Override 
    public void mouseExited(MouseEvent arg0) 
    { 
        requestFocusInWindow(false); 
    }
    
    private Vertex location(MouseEvent event) {
    	clickPoint = new Vertex((double) event.getX(), (double) event.getY());
		try {
			clickPoint.applyTransform(transform.createInverse());
		} catch (NoninvertibleTransformException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return clickPoint;
    }
    
    private void deselectVertices() {
    	for (Vertex previouslySelected : selected) {
			previouslySelected.selected = false;
		}
		selected.clear();
    }
    
    private void select(Vertex vertex) {
    	vertex.selected = true;
    	selected.add(vertex);
    }
 
	@Override
	public void mousePressed(MouseEvent event) {
		clickPoint = location(event);
		dragPoint = clickPoint.clone();
		if (event.isAltDown()) {
			selectionDrag = true;
			return;
		}
		if ( ! event.isShiftDown()) {
			deselectVertices();
		}
		boolean addVertex = true;
		for (Vertex tested : vertices) {
			boolean hit = tested.isInCircle(clickPoint, VERTEX_SIZE_HIGHLIGHTED);
			if (hit) {
				addVertex = false;
				select(tested);
				break;
			}
		}
		if (addVertex) {
			vertices.add(clickPoint.clone());
			triangulate(true);
		}
       
	}
 
    @Override 
    public void mouseReleased(MouseEvent arg0) 
    {
    	if (selectionDrag) {
			selectionDrag = false;
			int minX = Math.min(clickPoint.getX().intValue(), dragPoint.getX().intValue());
			int maxX = Math.max(clickPoint.getX().intValue(), dragPoint.getX().intValue());
			int minY = Math.min(clickPoint.getY().intValue(), dragPoint.getY().intValue());
			int maxY = Math.max(clickPoint.getY().intValue(), dragPoint.getY().intValue());
			int x,y;
			for (Vertex vertex : vertices) {
				x = vertex.getX().intValue();
				y = vertex.getY().intValue();
				if (x > minX && x < maxX && y > minY && y < maxY) {
					select(vertex);
				}
			}
			redraw();
			return;
		}
    	synchronized (computing) {
    		// we want immediate reaction.
    		computing = false;
    	}
    	if (triangulator != null && triangulator instanceof FastLMTTriangulator) {
    		((FastLMTTriangulator) triangulator).abort();
		}
		triangulate(true); 
    } 
 
    @Override 
    public void mouseDragged(MouseEvent event) 
    {
    	if (event.getX() >= 0 && event.getY() >= 0) {
    		Vertex delta = new Vertex((double)event.getX()/zoom, (double)event.getY()/zoom);
    		delta.minus(dragPoint);
    		dragPoint.plus(delta);
    		if (selectionDrag) {
				
			} else {
				for (Vertex sel : selected) 
	    		{
	    			sel.plus(delta);
	    			sel.round();
	    		}
	        
	    		if (event.getX() > getWidth()) {
	    			this.setPreferredSize(new Dimension(event.getX() + 10, getHeight()));
	    		}
	    		
	    		if (event.getY() > getHeight()) {
	    			this.setPreferredSize(new Dimension(getWidth(), event.getY() + 10));
	    		}
	    		if (vertices.size() < MAXIMUM_SIZE_FOR_INTERACTIVE_MODE) {
	    			triangulate(false); 
				}
			}
    		repaint();
    	}
    }
    
    private void displayCoordinates(Vertex vertex) {
    	if (vertexDisplayingCoordinates != null) {
			vertexDisplayingCoordinates.displayCoordinates = false;
		}
    	if (vertex != null) {
    		vertex.displayCoordinates = true;
		}
    	vertexDisplayingCoordinates = vertex;
    }
 
    @Override 
    public void mouseMoved(MouseEvent event) 
    {
    	clickPoint = location(event);
		for (Vertex tested : vertices) {
			boolean hit = tested.isInCircle(clickPoint, VERTEX_SIZE_HIGHLIGHTED);
			if (hit) {
				if ( event.isShiftDown()) {
					tested.selected = true;
					selected.add(tested);
				}
				displayCoordinates(tested);
				break;
			} else {
				displayCoordinates(null);
			}
		}
		repaint();
    } 
 
	@Override
	public void keyPressed(KeyEvent event) {
		if (event.getKeyCode() == KeyEvent.VK_DELETE) {
			for (Vertex sel : selected) {
				vertices.remove(sel);
			}
			int numSelected = selected.size();
			selected.clear();
			if (numSelected > 0) {
				triangulate(true);
			}
		}
		if (event.getKeyCode() == KeyEvent.VK_ESCAPE) {
			deselectVertices();
		}

		if (event.getKeyCode() == KeyEvent.VK_C) {
			if (event.isShiftDown()) {
				if (showCircles) {
					showCircles = false;
				} else {
					showCircles = true;
				}
				redraw();
			} else {
				pasteBuffer.clear();
				pasteBuffer.addAll(selected);
			}
		}
		if (event.getKeyCode() == KeyEvent.VK_V) {
			if (pasteBuffer.size() > 0) {
				deselectVertices();
				Set<Vertex> newPasteBuffer = new HashSet<Vertex>();
				Vertex offset = new Vertex(10.0, 10.0);
				for (Vertex vertex : pasteBuffer) {
					Vertex copy = vertex.clone();
					copy.translate(offset);
					newPasteBuffer.add(copy);
				}
				vertices.addAll(newPasteBuffer);
				pasteBuffer = newPasteBuffer;
				for (Vertex vertex : newPasteBuffer) {
					select(vertex);
				}
				triangulate(true);
			}
		}
		if (event.getKeyCode() == KeyEvent.VK_UP) {
			if (event.isShiftDown()) {
				double min = Double.MAX_VALUE, max = -Double.MAX_VALUE;
				double y;
				for (Vertex vertex : selected) {
					y = vertex.getY();
					max = Math.max(max, y);
					min = Math.min(min, y);
				}
				double center = min + (max - min)/2;
				for (Vertex vertex : selected) {
					y = vertex.getY();
					double offset = (center - y)*2;
					vertex.setY(y+offset);
				}
				triangulate(true);
			}
		}
		if (event.getKeyCode() == KeyEvent.VK_LEFT) {
			if (event.isShiftDown()) {
				double min = Double.MAX_VALUE, max = -Double.MAX_VALUE;
				double x;
				for (Vertex vertex : selected) {
					x = vertex.getX();
					max = Math.max(max, x);
					min = Math.min(min, x);
				}
				double center = min + (max - min)/2;
				for (Vertex vertex : selected) {
					x = vertex.getX();
					double offset = (center - x)*2;
					vertex.setX(x+offset);
				}
				triangulate(true);
			}
		}

	}
 
    @Override 
    public void keyReleased(KeyEvent arg0) 
    {     
    } 
 
    @Override 
    public void keyTyped(KeyEvent arg0) 
    { 
    }

	@Override
	public Dimension getPreferredScrollableViewportSize() {
		return new Dimension(10, 10);
	}

	@Override
	public int getScrollableBlockIncrement(Rectangle arg0, int arg1, int arg2) {
		// TODO Auto-generated method stub
		return 5;
	}

	@Override
	public boolean getScrollableTracksViewportHeight() {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public boolean getScrollableTracksViewportWidth() {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public int getScrollableUnitIncrement(Rectangle arg0, int arg1, int arg2) {
		// TODO Auto-generated method stub
		return 5;
	}

	
	@SuppressWarnings("unchecked")
	@Override
	public void receiveNotification(Notification notification) {
		
		if (notification.name == Notification.didFindEdgeIn) {
			if ( ! (notification.sender instanceof List<?>)) {
				return;
			}
			synchronized (this) {
				edges = (List<Edge>) notification.sender;
//				this.redraw();
			}
		}
		if (notification.name == Notification.didFinishMWT) {
			this.redraw();
			mainWindow.getBottomPanel().updateProgress("", 100);
			
			if ( ! (notification.sender instanceof List<?>)) {
				return;
			}
			edges = (List<Edge>) notification.sender;
			this.computing = false;
			this.processChanges();
		}
		if (notification.name == Notification.didTriangulatePolygon) {
			if ( ! (notification.sender instanceof List<?>)) {
				return;
			}
			synchronized (edges) {
				edges.addAll((List<Edge>) notification.sender);
				this.redraw();
			}
		}
		if (notification.name == Notification.didFindCircles) {
			circles = (List<Circle>) notification.sender;
		}
//		if (notification.name == Notification.didTerminateTriangulator) {
//			System.out.println("aborted");
//			triangulate(true);
//		}
	}
}
